using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace HistogramPackage
{
    public abstract class Descriptor
    {
        protected bool _dirty = true;
        private Descriptor prevState;

        public abstract Descriptor Clone();

        public virtual bool Equals(Descriptor obj)
        {
            if (obj == null || GetType() != obj.GetType())
                return false;

            return true;
        }

        public void Invalidate() { _dirty = true; }

        public bool Stale()
        {
            if (_dirty)
                return true;

            return !Equals(prevState);
        }

        public void Refresh()
        {
            prevState = this.Clone();
            _dirty = false;
        }
    }

    // collects various PDF information
    public class PDFPlotDescriptor : Descriptor
    {
        public readonly static Color DEF_PLOT_COLOR = Color.Red;
        public readonly static double DEF_RANGE_START = 0.0;
        public readonly static double DEF_RANGE_END = 1.0;
        public readonly static int DEF_NUM_POINTS = 256;

        public PDF PDF;
        public string Name; // defaults to null
        private double[] _points;
        public double Start = DEF_RANGE_START;
        public double End = DEF_RANGE_END;
        public int NumPoints = DEF_NUM_POINTS;
        private double _maximum = 0;
        public Color PlotColor = DEF_PLOT_COLOR;

        public double Maximum
        {
            get
            {
                if (Stale())
                    Plot();
                return _maximum;
            }
        }
        public double[] Points
        {
            get
            {
                if (Stale())
                    Plot();
                return _points;
            }
        }

        public PDFPlotDescriptor(PDF newPDF, double rangeStart, double rangeEnd, int numPoints, Color plotColor, string name = null)
        {
            this.PDF = newPDF;
            this.Start = rangeStart;
            this.End = rangeEnd;
            this.NumPoints = numPoints;
            this.PlotColor = plotColor;
            this.Name = name;
        }

        public PDFPlotDescriptor(PDF pdf)
        {
            this.PDF = pdf;
        }

        public PDFPlotDescriptor(PDFPlotDescriptor clone)
        {
        }

        public override Descriptor Clone()
        {
            return new PDFPlotDescriptor(this);
        }

        public override bool Equals(Descriptor obj)
        {
            return base.Equals(obj)
                && this.Start.Equals(((PDFPlotDescriptor)obj).Start)
                && this.End.Equals(((PDFPlotDescriptor)obj).End)
                && this.NumPoints.Equals(((PDFPlotDescriptor)obj).NumPoints)
                && this._maximum.Equals(((PDFPlotDescriptor)obj)._maximum)
                && this.Name.Equals(((PDFPlotDescriptor)obj).Name)
                && this._points != null
                && this._points.Count().Equals(this.NumPoints)
                && this.PlotColor.Equals(((PDFPlotDescriptor)obj).PlotColor)
                && this.PDF.Equals(((PDFPlotDescriptor)obj).PDF)
            ;
        }

        private void Plot()
        {
            _points = new double[NumPoints];
            double val;
            RangeTranslator fit = new RangeTranslator(0, NumPoints - 1, Start, End);

            for (int b = 0; b < NumPoints; b++)
            {
                val = PDF.getOutput(fit.Translate(b));
                _points[b] = val;

                if (val > _maximum) _maximum = val;
            }
            _dirty = false;
        }
    }

    /*
     *	a class representing histogram drawing information
     */
    public class HistogramDescriptor : Descriptor
    {
        public static readonly Color DEF_TEXT_COLOR = Color.Black;
        public static readonly Color DEF_AXIS_COLOR = Color.Black;
        public static readonly Color DEF_OUTLINE_COLOR = Color.Black;
        public static readonly Color DEF_FILL_GRADIENT_START = Color.Blue;
        public static readonly Color DEF_FILL_GRADIENT_END = Color.LightBlue;
        public static readonly float DEF_LINE_WEIGHT = 2;
        public static readonly int DEF_PDF_GRANULARITY = 2;

        public Size ImageSize;
        public Color AxisColor = DEF_AXIS_COLOR;
        public Color TextColor = DEF_TEXT_COLOR;
        public Color OutlineColor = DEF_OUTLINE_COLOR;
        public Color FillGradientStart = DEF_FILL_GRADIENT_START;
        public Color FillGradientEnd = DEF_FILL_GRADIENT_END;
        public float LineWeight = DEF_LINE_WEIGHT;
        public int PDFGranularity = DEF_PDF_GRANULARITY;

        public int Width { get { return ImageSize.Width; } set { ImageSize.Width = value; } }
        public int Height { get { return ImageSize.Height; } set { ImageSize.Height = value; } }

        public HistogramDescriptor(int width, int height)
        {
            this.ImageSize = new Size(width, height);
        }

        public HistogramDescriptor(Size size)
        {
            this.ImageSize = size;
        }

        // deprecated constructor
        public HistogramDescriptor(int width, int height, Color fillStart, Color fillEnd, Color outlineColor, int lineWeight)
        {
            this.ImageSize = new Size(width, height);
            this.OutlineColor = outlineColor;
            this.FillGradientStart = fillStart;
            this.FillGradientEnd = fillEnd;
            this.LineWeight = lineWeight;
        }

        public HistogramDescriptor(HistogramDescriptor clone)
        {
            this.ImageSize = new Size(clone.Width, clone.Height);
            this.AxisColor = clone.AxisColor;
            this.TextColor = clone.TextColor;
            this.OutlineColor = clone.OutlineColor;
            this.FillGradientStart = clone.FillGradientStart;
            this.FillGradientEnd = clone.FillGradientEnd;
            this.LineWeight = clone.LineWeight;
            this.PDFGranularity = clone.PDFGranularity;
        }

        public override Descriptor Clone()
        {
            return new HistogramDescriptor(this);
        }

        public override bool Equals(Descriptor that)
        {
            HistogramDescriptor other = (HistogramDescriptor)that;
            return base.Equals(that)
                && this.ImageSize.Equals(((HistogramDescriptor)that).ImageSize)
                && this.AxisColor.Equals(((HistogramDescriptor)that).AxisColor)
                && this.TextColor.Equals(((HistogramDescriptor)that).TextColor)
                && this.OutlineColor.Equals(((HistogramDescriptor)that).OutlineColor)
                && this.FillGradientStart.Equals(((HistogramDescriptor)that).FillGradientStart)
                && this.FillGradientEnd.Equals(((HistogramDescriptor)that).FillGradientEnd)
                && this.LineWeight.Equals(((HistogramDescriptor)that).LineWeight)
                && this.PDFGranularity.Equals(((HistogramDescriptor)that).PDFGranularity)
            ;
        }
    }

    /*
     *	a simple class representing histogram data information
     */
    public class BucketsDescriptor : Descriptor
    {
        readonly static int DEFAULT_BAR_COUNT = 40;
        readonly static double DEFAULT_RANGE_START = 0.0;
        readonly static double DEFAULT_RANGE_END = 1.0;

        public int BucketCount = DEFAULT_BAR_COUNT;
        public double Start = DEFAULT_RANGE_START;
        public double End = DEFAULT_RANGE_END;

        public BucketsDescriptor() { }

        public BucketsDescriptor(int bucketCount, double start, double end)
        {
            this.BucketCount = bucketCount;
            this.Start = start;
            this.End = end;
        }

        public BucketsDescriptor(BucketsDescriptor clone)
        {
            this.BucketCount = clone.BucketCount;
            this.Start = clone.Start;
            this.End = clone.End;
        }

        public override Descriptor Clone()
        {
            return new BucketsDescriptor(this);
        }

        // compare two BucketsDescriptors for value equality
        public override bool Equals(object obj)
        {
            BucketsDescriptor other = (BucketsDescriptor)obj;
            return base.Equals(obj)
                && this.BucketCount.Equals(other.BucketCount)
                && this.Start.Equals(other.Start)
                && this.End.Equals(other.End)
            ;
        }
    }

    /*
     *	An immutable collection of buckets.  This class is used by the Histogram
    *	class defined below to represent the bars in the histogram graph bitmap.
    *	This class only maintains the count associated with each bucket; all
    *	color and bitmap information is handled by the Histogram class itself.
    */
    public class BucketList
    {
        private double _leftEdge;
        private double _rightEdge;
        private int _maxBucket;
        private long _bucketTotal;
        private int[] _buckets;

        public double LeftEdge { get { return _leftEdge; } }
        public double RightEdge { get { return _rightEdge; } }
        public int LargestBucket { get { return _maxBucket; } }
        public int[] Buckets { get { return _buckets; } }
        public int BucketCount { get { return _buckets.Length; } }
        public long SumTotal { get { return _bucketTotal; } }

        public BucketList()
        {
            _leftEdge = 0.0;
            _rightEdge = 1.0;
            _maxBucket = 0;
        }

        public BucketList(SortedDictionary<double, int> dataset, int bucketCount)
        {
            this.ImportDataset(dataset, bucketCount, dataset.First().Key, dataset.Last().Key);
        }

        public BucketList(SortedDictionary<double, int> dataset, int bucketCount, double start, double end)
        {
            this.ImportDataset(dataset, bucketCount, start, end);
        }

        public BucketList(SortedDictionary<double, int> dataset, BucketsDescriptor bucketStats)
        {
            this.ImportDataset(dataset, bucketStats.BucketCount, bucketStats.Start, bucketStats.End);
        }

        // collect a dataset into buckets
        private void ImportDataset(SortedDictionary<double, int> dataset, int bucketCount, double start, double end)
        {
            _maxBucket = 0;
            _bucketTotal = 0;
            _leftEdge = start;
            _rightEdge = end;
            _buckets = new int[bucketCount];

            int currentBar = 0;
            double barWidth = (_rightEdge - _leftEdge) / bucketCount;
            double bucketLimit = _leftEdge + barWidth;
            foreach (KeyValuePair<double, int> sample in dataset)
            {
                // skip samples out of range
                if (sample.Key < _leftEdge)
                    continue;

                if (sample.Key > bucketLimit)
                {   // we're done with this bucket

                    // skip samples out of range
                    if (sample.Key >= _rightEdge)
                        return;

                    // start a new bucket
                    currentBar = (int)Math.Floor((sample.Key - _leftEdge) / barWidth);
                    bucketLimit = _leftEdge + (currentBar + 1) * barWidth;
                }

                // add the sample to the current bucket
                _buckets[currentBar] += sample.Value;

                // check for new maximum bucket
                if (_buckets[currentBar] > _maxBucket)
                    _maxBucket = _buckets[currentBar];

                // total up buckets
                // WARN: global max, or ranged max??
                _bucketTotal += sample.Value;
            }
        }

        // (implicitly) converts a BucketList to a BucketsDescriptor
        public static implicit operator BucketsDescriptor(BucketList buckets)
        {
            if (buckets == null)
                return null;
            else
                return new BucketsDescriptor(buckets.BucketCount, buckets.LeftEdge, buckets.RightEdge);
        }
    }

    /*
     *	This Histogram class represents a histogram bitmap.  The two most
     *	expensive operations, filling buckets and drawing the image, are only
     *	performed as needed.  This class supports multiple PDF overlays while
     *	maintaining compatibility with the previous version.
     */
    public class Histogram
    {

        public enum NegativeHandle
        {
            TRUNCATE,
            SHIFT
        }

        public enum FontTrim
        {
            TRIM,
            WIDE
        }

        private readonly static int VERT_TICKS = 5;
        private readonly static double VERT_RES = 0.0625;
        private readonly static int TICK_LENGTH = 7;
        private readonly static float MINOR_TICK_SCALE = 0.4f;
        private readonly static string HORI_LABEL_FORMAT = "G";
        private readonly static string VERT_LABEL_FORMAT = "0.0###";
        private readonly static int MAX_LABEL_PRECISION = 5;

        readonly static Color GRAPH_BACKGROUND_COLOR = Color.White;
        readonly static int MARGIN_PIXELS = 10;
        readonly static byte LEGEND_OPACITY = 0x8f;
        readonly static float LABEL_FONT_SIZE = 7.5f;
        readonly static Font LABEL_FONT = new Font(FontFamily.GenericSansSerif, LABEL_FONT_SIZE);

        private const int DEFAULT_WIDTH = 256;
        private const int DEFAULT_HEIGHT = 128;
        private const float DEFAULT_LINE_WEIGHT = 2;
        private const int DEFAULT_BAR_COUNT = 40;
        private const double DEFAULT_RANGE_START = 0.0;
        private const double DEFAULT_RANGE_END = 1.0;

        // local data
        private SortedDictionary<double, int> _dataset;
        private BucketList _buckets;
        private Dictionary<PDF, PDFPlotDescriptor> _pdfs = new Dictionary<PDF, PDFPlotDescriptor>();
        private Bitmap _image;

        // image properties
        private HistogramDescriptor graphStats = new HistogramDescriptor(DEFAULT_WIDTH, DEFAULT_HEIGHT);
        private BucketsDescriptor _bucketStats = new BucketsDescriptor();

        public int BucketCount { get { return _bucketStats.BucketCount; } set { _bucketStats.BucketCount = value; } }
        public int BarCount { get { return _bucketStats.BucketCount; } set { _bucketStats.BucketCount = value; } }
        public double Start { get { return _bucketStats.Start; } set { _bucketStats.Start = value; } }
        public double End { get { return _bucketStats.End; } set { _bucketStats.End = value; } }
        public Size ImageSize { get { return graphStats.ImageSize; } set { graphStats.ImageSize = value; } }
        public int ImageWidth { get { return graphStats.Width; } set { graphStats.Width = value; } }
        public int ImageHeight { get { return graphStats.Height; } set { graphStats.Height = value; } }
        public Color FillGradientStart { get { return graphStats.FillGradientStart; } set { graphStats.FillGradientStart = value; } }
        public Color FillGradientEnd { get { return graphStats.FillGradientEnd; } set { graphStats.FillGradientEnd = value; } }
        public Color OutlineColor { get { return graphStats.OutlineColor; } set { graphStats.OutlineColor = value; } }
        public float LineWeight { get { return graphStats.LineWeight; } set { graphStats.LineWeight = value; } }
        public Bitmap GraphBitmap { get { return GetHistogram(); } }

        // default constructor
        public Histogram() { }

        // simple constructors
        public Histogram(Dictionary<double, int> dataset) { this.ImportData(dataset); }
        public Histogram(List<Tuple<double, int>> dataset) { this.ImportData(dataset); }

        // quick constructor
        public Histogram(Dictionary<double, int> dataset, int barCount, Color fillColor)
        {
            this.graphStats.FillGradientStart = this.graphStats.FillGradientEnd = fillColor;
            this.ImportData(dataset);
        }

        // compatibility constructors
        public Histogram(Dictionary<double, int> dataset, bool sorted = false, int barCount = DEFAULT_BAR_COUNT, NegativeHandle handler = NegativeHandle.TRUNCATE)
        {
            _bucketStats.BucketCount = barCount;
            this.ImportData(dataset, handler);
        }

        public Histogram(List<Tuple<double, int>> dataset, bool sorted = false, int barCount = DEFAULT_BAR_COUNT, NegativeHandle handler = NegativeHandle.TRUNCATE)
        {
            _bucketStats.BucketCount = barCount;
            this.ImportData(dataset, handler);
        }

        // import a dictionary of value-frequency pairs into a (sorted) dataset
        public void ImportData(Dictionary<double, int> dataset, NegativeHandle handler = NegativeHandle.TRUNCATE)
        {
            if (dataset.Values.Min() < 0)
                throw new ArgumentOutOfRangeException("dataset", "Found negative population in dataset.");

            _dataset = new SortedDictionary<double, int>(dataset);

            if (_dataset.Count > 0)
            {
                double minKey = _dataset.First().Key;
                if (_dataset.First().Key < 0)
                    if (handler == NegativeHandle.TRUNCATE)
                    {
                        // remove all negative data points
                        double firstKey = _dataset.Keys.First();
                        while (firstKey < 0)
                        {
                            _dataset.Remove(firstKey);
                            firstKey = _dataset.Keys.First();
                        }
                        //_bucketStats.Start = _dataset.First(sample => sample.Key >= 0).Key;
                    }
                    else
                    {
                        // shift the entire dataset out of negative
                        SortedDictionary<double, int> shiftSet = new SortedDictionary<double, int>();
                        foreach (KeyValuePair<double, int> kvp in _dataset)
                            shiftSet.Add(kvp.Key + minKey, kvp.Value);
                        _dataset = shiftSet;
                    }

                _bucketStats.Start = _dataset.First().Key;
                _bucketStats.End = _dataset.Last().Key;
            }

            // Importing a dataset always invalidates the image; this ensures
            // we redraw the image when the data set changes, even if the
            // BucketsDescriptor stays the same.
            graphStats.Invalidate();
        }

        // import a list of data points into a dictionary
        public void ImportData(List<Tuple<double, int>> dataset, NegativeHandle handler = NegativeHandle.TRUNCATE)
        {
            Dictionary<double, int> importSet = new Dictionary<double, int>();

            // individually copy each element
            foreach (Tuple<double, int> b in dataset)
            {
                if (b.Item2 < 0)
                    throw new ArgumentOutOfRangeException("dataset", "Found negative frequency in dataset.");

                if (importSet.ContainsKey(b.Item1))
                    importSet[b.Item1] += b.Item2;
                else
                    importSet.Add(b.Item1, b.Item2);
            }

            this.ImportData(importSet, handler);
        }

        // adds a single PDF to the histogram
        public void AddPDF(PDF newPDF, Color pdfColor, string pdfName = null)
        {
            // do we already have this PDF?
            if (_pdfs.ContainsKey(newPDF))
            {
                PDFPlotDescriptor thisPDF = _pdfs[newPDF];

                // PDF already in list
                if (thisPDF.PlotColor != pdfColor || thisPDF.Name != pdfName)
                {
                    // info differs, update PDF
                    thisPDF.PlotColor = pdfColor;
                    thisPDF.Name = pdfName;
                    graphStats.Invalidate();
                }
            }
            else
            {
                // PDF not yet in list
                PDFPlotDescriptor pdfInfo = new PDFPlotDescriptor(newPDF);
                pdfInfo.PlotColor = pdfColor;
                pdfInfo.Name = pdfName;

                _pdfs[newPDF] = pdfInfo;
                graphStats.Invalidate();
            }
        }

        // removes all PDFs from the histogram
        public void ClearPDFs()
        {
            // we don't want to redraw if the PDF list is already empty
            if (_pdfs.Count != 0)
            {
                _pdfs.Clear();
                graphStats.Invalidate();
            }
        }

        public Bitmap GetHistogram()
        {
            // check for stale bitmap
            if (!_bucketStats.Equals(_buckets))
            {
                _buckets = new BucketList(_dataset, _bucketStats);
                this.RenderHistogram();
            }
            else if (graphStats.Stale())
                this.RenderHistogram();

            return _image;
        }

        /*
         *	Performs the actual drawing.
         */
        private void RenderHistogram()
        {
            /*
             * Compute Before Drawing
             */

            // determine maximum width of vertical axis labels
            //float labelWidth = stringWidth(new string('0', (int)Math.Ceiling(Math.Log10(_buckets.LargestBucket))), LABEL_FONT);
            float vLabelWidth = stringWidth("0.0000", LABEL_FONT) * 1.1f;

            int margin = MARGIN_PIXELS;
            int graphBottom = graphStats.ImageSize.Height - TICK_LENGTH - LABEL_FONT.Height - margin;
            int graphHeight = graphBottom - (int)Math.Ceiling(0.5 * LABEL_FONT.Height) - margin;
            int graphLeft = margin + (int)vLabelWidth + TICK_LENGTH;
            int graphWidth = graphStats.ImageSize.Width - graphLeft - margin;

            double maxY = ((double)_buckets.LargestBucket / _buckets.SumTotal);
            double graphSpan = _buckets.RightEdge - _buckets.LeftEdge;

            // find largest PDF value and name width; also compute PDF values as needed
            float maxPDFNameLen = 0;
            int legendCount = 0;
            {
                int numPoints = graphWidth / graphStats.PDFGranularity;
                double xStep = graphSpan / graphWidth;
                double firstSample = _buckets.LeftEdge + xStep / 2;
                double lastSample = _buckets.RightEdge - xStep / 2;

                foreach (PDFPlotDescriptor thisPlot in _pdfs.Values)
                {
                    thisPlot.Start = firstSample;
                    thisPlot.End = lastSample;
                    thisPlot.NumPoints = numPoints;
                    maxY = Math.Max(maxY, thisPlot.Maximum);

                    if (thisPlot.Name != null)
                    {
                        maxPDFNameLen = Math.Max(stringWidth(thisPlot.Name.ToString(), LABEL_FONT), maxPDFNameLen);
                        legendCount++;
                    }
                }
            }

            // normalize vertical graph scale
            maxY = Math.Max(VERT_RES * Math.Ceiling(maxY / VERT_RES), VERT_RES);
            maxY = Math.Min(maxY, 1.0);
            double yScale = (double)graphHeight / maxY;

            // plot PDFs
            List<Plot> pdfPlots = new List<Plot>();
            {
                RangeTranslator yFit;
                RangeTranslator xFit;
                Plot newPlot;
                double[] points;
                foreach (PDFPlotDescriptor thisPDF in _pdfs.Values)
                {
                    yFit = new RangeTranslator(0, maxY, graphBottom, graphBottom - graphHeight);
                    points = thisPDF.Points;
                    xFit = new RangeTranslator(0, points.Count() - 1, graphLeft + (float)graphStats.PDFGranularity / 2, graphLeft + graphWidth - (float)graphStats.PDFGranularity / 2);
                    newPlot = new Plot();
                    newPlot.Pen = new Pen(thisPDF.PlotColor, graphStats.LineWeight * 2);
                    newPlot.Points = new PointF[points.Count()];

                    for (int p = 0; p < points.Count(); p++)
                        newPlot.Points[p] = new PointF((float)xFit.Translate(p), (float)yFit.Translate(points[p]));

                    pdfPlots.Add(newPlot);
                }
            }

            // determine number of vertical ticks
            int verticalTicks;
            {
                if (graphHeight < VERT_TICKS * (LABEL_FONT.Height + margin))
                {
                    verticalTicks = (int)Math.Floor((double)graphHeight / (LABEL_FONT.Height + margin));
                    if (verticalTicks < 1) verticalTicks = 1;
                }
                else
                    verticalTicks = VERT_TICKS;
            }

            // determine maximum horizontal tick label width
            double xMajor;
            double xMinor;
            int horizontalPrecision = MAX_LABEL_PRECISION;
            {
                float hLabelWidth = stringWidth("0.0E+00", LABEL_FONT) * 1.25f;

                // determine number of ticks
                int stepExponent = (int)Math.Ceiling(Math.Log10(graphSpan * hLabelWidth / graphWidth)) - 1;
                xMinor = Math.Pow(10, stepExponent);
                xMajor = xMinor * 10;

                if (hLabelWidth * graphSpan / xMajor * 5 < graphWidth)
                    xMajor /= 5;
                else if (hLabelWidth * graphSpan / xMajor * 2 < graphWidth)
                    xMajor /= 2;
            }

            /*
             * BEGIN DRAWING
             */

            // set up the drawing canvas
            _image = new Bitmap(graphStats.ImageSize.Width, graphStats.ImageSize.Height);
            Graphics g = Graphics.FromImage(_image);
            g.SmoothingMode = SmoothingMode.HighQuality;
            g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
            g.Clear(GRAPH_BACKGROUND_COLOR);

            // draw the axis
            Pen axisPen = new Pen(graphStats.AxisColor, graphStats.LineWeight);
            g.DrawLine(axisPen, graphLeft, graphBottom, graphLeft + graphWidth, graphBottom); // horizontal axis
            g.DrawLine(axisPen, graphLeft, graphBottom, graphLeft, graphBottom - graphHeight); // vertical axis

            // draw tick marks
            StringFormat labelFormat = new StringFormat();

            { // draw vertical ticks
                RectangleF labelRect;

                labelFormat.Alignment = StringAlignment.Far;
                float vPoint;
                for (int t = 0; t <= verticalTicks; t++)
                {
                    vPoint = graphBottom - graphHeight * t / verticalTicks;
                    labelRect = new RectangleF(graphLeft - vLabelWidth - TICK_LENGTH, vPoint - LABEL_FONT.Height * 0.5f, vLabelWidth, LABEL_FONT.Height);

                    g.DrawLine(axisPen, labelRect.Right, vPoint, graphLeft, vPoint);
                    g.DrawString((maxY * t / verticalTicks).ToString(VERT_LABEL_FORMAT), LABEL_FONT, Brushes.Black, labelRect, labelFormat);
                }
            }

            { // draw horizontal ticks
                float hPoint;
                RangeTranslator fit;

                // draw minor ticks
                fit = new RangeTranslator(_buckets.LeftEdge, _buckets.RightEdge, graphLeft, graphLeft + graphWidth);
                Pen minorPen = new Pen(graphStats.AxisColor, LineWeight / 2);
                for (double xVal = xMinor * Math.Ceiling(_buckets.LeftEdge / xMinor); xVal <= _buckets.RightEdge; xVal += xMinor)
                {
                    hPoint = (float)fit.Translate(xVal);
                    g.DrawLine(minorPen, hPoint, graphBottom, hPoint, graphBottom + TICK_LENGTH * MINOR_TICK_SCALE);
                }

                // draw major ticks
                string formatString = HORI_LABEL_FORMAT + horizontalPrecision.ToString();
                labelFormat.Alignment = StringAlignment.Center;
                float labelOffset = 0.0f - stringWidth("0", LABEL_FONT) / 2;
                if (graphSpan / xMajor < 2)
                { // only label start and end of graph
                    // left edge label
                    g.DrawLine(axisPen, graphLeft, graphBottom, graphLeft, graphBottom + TICK_LENGTH);
                    g.DrawString(_buckets.LeftEdge.ToString(formatString), LABEL_FONT, Brushes.Black, graphLeft + labelOffset, graphBottom + TICK_LENGTH);

                    // right edge label
                    string rightLabel = _buckets.RightEdge.ToString(formatString);
                    hPoint = graphLeft + graphWidth - stringWidth(rightLabel, LABEL_FONT, FontTrim.TRIM);
                    g.DrawLine(axisPen, graphLeft + graphWidth, graphBottom, graphLeft + graphWidth, graphBottom + TICK_LENGTH);
                    g.DrawString(rightLabel, LABEL_FONT, Brushes.Black, hPoint, graphBottom + TICK_LENGTH);
                }
                else
                { // variable number of major labels
                    float thisWidth;
                    for (double xVal = xMajor * Math.Ceiling(_buckets.LeftEdge / xMajor); xVal <= _buckets.RightEdge; xVal += xMajor)
                    {
                        hPoint = (float)fit.Translate(xVal);

                        // draw major tick
                        g.DrawLine(axisPen, hPoint, graphBottom, hPoint, graphBottom + TICK_LENGTH);

                        // draw tick label
                        thisWidth = stringWidth(xVal.ToString(formatString), LABEL_FONT, FontTrim.TRIM) / 2;

                        if (hPoint + thisWidth > graphLeft + graphWidth)
                            hPoint = graphLeft + graphWidth - thisWidth;

                        if (hPoint - thisWidth < graphLeft)
                            hPoint = graphLeft + thisWidth + labelOffset;

                        g.DrawString(xVal.ToString(formatString), LABEL_FONT, Brushes.Black, hPoint, graphBottom + TICK_LENGTH, labelFormat);
                    }
                }
            }

            // set up graph drawing space
            g.Clip = new Region(new Rectangle(graphLeft + 1, graphBottom - graphHeight, graphWidth, graphHeight));

            { // draw bar graph
                int thisBucket;
                RectangleF thisBar;
                Pen outlinePen = new Pen(graphStats.OutlineColor, graphStats.LineWeight);
                LinearGradientBrush gradientBrush;

                float barHeight;
                float barWidth = (float)graphWidth / _buckets.BucketCount;
                double barScale = (double)graphHeight / _buckets.SumTotal / maxY;
                for (int i = 0; i < _buckets.BucketCount; i++)
                {
                    thisBucket = _buckets.Buckets[i];

                    // don't draw empty buckets
                    if (thisBucket > 0)
                    {
                        barHeight = (float)(barScale * thisBucket);
                        thisBar = new RectangleF(barWidth * i + graphLeft, graphBottom - (float)barHeight, barWidth, (float)barHeight);

                        gradientBrush = new LinearGradientBrush(thisBar, graphStats.FillGradientStart, graphStats.FillGradientEnd, 0.0f);

                        // draw the bar
                        g.FillRectangle(gradientBrush, thisBar);
                        g.DrawRectangle(outlinePen, thisBar.Left, thisBar.Top, thisBar.Width, thisBar.Height);
                    }
                }
            }

            { // plot each PDF
                foreach (Plot thisPlot in pdfPlots)
                    g.DrawLines(thisPlot.Pen, thisPlot.Points);

                // draw legend
                float xPoint;
                float yPoint = graphBottom - graphHeight;
                float vSpacing = LABEL_FONT.Height;
                float radius = LABEL_FONT.Height * 0.5f;
                Pen thinPen = new Pen(graphStats.TextColor, graphStats.LineWeight / 2);
                Brush pdfBrush;

                Brush washout;
                {
                    int argb = GRAPH_BACKGROUND_COLOR.ToArgb();
                    argb &= 0x00ffffff | (LEGEND_OPACITY << 24);
                    washout = new SolidBrush(Color.FromArgb(argb));
                }


                xPoint = graphLeft + graphWidth - maxPDFNameLen - radius * 1.5f;
                if (xPoint < graphLeft + margin)
                    xPoint = graphLeft + margin;

                if (legendCount > 0)
                    g.FillRectangle(washout, xPoint, yPoint, radius * 1.5f + maxPDFNameLen, vSpacing * legendCount);

                foreach (PDFPlotDescriptor thisPDF in _pdfs.Values)
                {
                    if (thisPDF.Name == null)
                        continue;

                    pdfBrush = new SolidBrush(thisPDF.PlotColor);
                    g.FillEllipse(pdfBrush, xPoint, yPoint + (vSpacing - radius) / 2, radius, radius);
                    g.DrawEllipse(thinPen, xPoint, yPoint + (vSpacing - radius) / 2, radius, radius);
                    g.DrawString(thisPDF.Name.ToString(), LABEL_FONT, Brushes.Black, xPoint + radius * 1.5f, yPoint);
                    yPoint += vSpacing;
                }
            }

            // clean up
            g.Dispose();

            // synchronize states
            graphStats.Refresh();
        }

        /*
         *  utility function that measures the width of a string
         */
        private float stringWidth(string mStr, Font strFont, FontTrim trim = FontTrim.WIDE)
        {
            Graphics g = Graphics.FromImage(new Bitmap(graphStats.ImageSize.Width, strFont.Height));
            g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
            float imgWidth;

            if (trim == FontTrim.TRIM)
            {
                imgWidth = (float)g.MeasureString(mStr, strFont, new PointF(0.0f, 0.0f), StringFormat.GenericTypographic).Width;
            }
            else
                imgWidth = (float)g.MeasureString(mStr, strFont).Width;

            g.Dispose();
            return imgWidth;
        }

        /*
         *	The following methods provide compatibility with previous version
         *	of the Histogram class.
         */

        public Bitmap GetHistogramWithBucketCount(Size size, int barCount, PDF pdf = null, bool isMixed = false, float lineWeight = DEFAULT_LINE_WEIGHT)
        {
            // push relevant histogram info
            Dictionary<PDF, PDFPlotDescriptor> holdPDFs = _pdfs;
            HistogramDescriptor holdGraph = graphStats;
            BucketsDescriptor holdBucket = _bucketStats;

            // set temporary histogram info
            _bucketStats = new BucketsDescriptor(barCount, holdBucket.Start, holdBucket.End);
            _bucketStats.BucketCount = barCount;

            graphStats = new HistogramDescriptor(holdGraph);
            graphStats.ImageSize = size;
            graphStats.LineWeight = lineWeight;

            _pdfs = new Dictionary<PDF, PDFPlotDescriptor>();
            if (pdf != null)
                this.AddPDF(pdf, PDFPlotDescriptor.DEF_PLOT_COLOR);

            // draw it
            Bitmap hist = this.GetHistogram();

            // pop original histogram info
            _pdfs = holdPDFs;
            graphStats = holdGraph;
            _bucketStats = holdBucket;

            return hist;
        }

        public Bitmap GetHistogramBitmapWithColor(Size size, Color color, PDF pdf = null, bool isMixed = false, float lineWeight = DEFAULT_LINE_WEIGHT)
        {
            // push relevant histogram info
            Dictionary<PDF, PDFPlotDescriptor> holdPDFs = _pdfs;
            HistogramDescriptor holdGraph = graphStats;

            // set temporary histogram info
            graphStats = new HistogramDescriptor(holdGraph);
            graphStats.ImageSize = size;
            graphStats.FillGradientStart = color;
            graphStats.FillGradientEnd = color;
            graphStats.LineWeight = lineWeight;

            _pdfs = new Dictionary<PDF, PDFPlotDescriptor>();
            if (pdf != null)
                this.AddPDF(pdf, PDFPlotDescriptor.DEF_PLOT_COLOR);

            // draw it
            Bitmap hist = this.GetHistogram();

            // pop original histogram info
            _pdfs = holdPDFs;
            graphStats = holdGraph;

            return hist;
        }

        public Bitmap GetHistogramBitmap(Size size, string title = "", PDF pdf = null, bool isMixed = false, float lineWeight = DEFAULT_LINE_WEIGHT)
        {
            return GetHistogramBitmap(size, pdf, lineWeight);
        }

        public Bitmap GetHistogramBitmap(Size size, PDF pdf, float lineWeight = DEFAULT_LINE_WEIGHT)
        {
            // push relevant histogram info
            Dictionary<PDF, PDFPlotDescriptor> holdPDFs = _pdfs;
            HistogramDescriptor holdGraph = graphStats;

            // set temporary histogram info
            graphStats = new HistogramDescriptor(holdGraph);
            graphStats.ImageSize = size;
            graphStats.LineWeight = lineWeight;

            _pdfs = new Dictionary<PDF, PDFPlotDescriptor>();
            if (pdf != null)
                this.AddPDF(pdf, PDFPlotDescriptor.DEF_PLOT_COLOR);

            // draw it
            Bitmap hist = this.GetHistogram();

            // pop original histogram info
            _pdfs = holdPDFs;
            graphStats = holdGraph;

            return hist;
        }
    }

    public struct Plot
    {
        public PointF[] Points;
        public Pen Pen;
    }

    /*
     *  utility class to translate a value in one range to another
     */
    public class RangeTranslator
    {
        private double _srcOffset = 0;
        private double _dstOffset = 0;
        private double _scale = 1;

        public RangeTranslator() { }
        public RangeTranslator(double srcStart, double srcEnd, double dstStart, double dstEnd)
        {
            _srcOffset = srcStart;
            _dstOffset = dstStart;
            _scale = (dstEnd - dstStart) / (srcEnd - srcStart);
        }

        public double Translate(double val)
        {
            return _dstOffset + (val - _srcOffset) * _scale;
        }
    }
}
